// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! A driver for the STM32xx RCC, GPIO, SYSCFG, and EXTI blocks, combined for
//! compactness.
//!
//!
//! # RCC+GPIO theory of operation
//!
//! The RCC and GPIO bits basically just expose the following hardware
//! operations via IPC:
//!
//! - Giving callers control over clock gating (enabling / disabling clock
//! signals to peripherals, since most are off by default),
//!
//! - Controlling peripheral reset signals,
//!
//! - Configuring GPIO pins (assigning to a peripheral or making into a direct
//! input or output),
//!
//! - Reading and writing direct input/outputs.
//!
//! Currently we expose all of this to all callers. This is a little
//! loosey-goosey, and we could add some notion of static ownership or access
//! control, but in practice it hasn't been a problem. If we get tasks e.g.
//! fighting over who owns a GPIO pin, or resetting each others' peripherals, we
//! might reconsider this.
//!
//!
//! # EXTI theory of operation
//!
//! `sys` serves as a multiplexer for pin-change interrupts generated by the
//! EXTI unit. From a user perspective, this works as follows:
//!
//! 1. `sys` gets configured in the `app.toml` to route interrupts on a specific
//!    pin to a specific task, using a specific notification. See [the section
//!    on EXTI configuration](#configuring-exti-notification) for details.
//! 2. That task configures the pin using `sys.gpio_configure` and friends
//!    (probably as an input, possibly with a pull resistor).
//! 3. That task _also_ configures the pin's edge sensitivity, using
//!    `sys.gpio_irq_configure`. Instead of naming the pin in this API, the task
//!    needs to pass the notification mask where it expects the IRQ to arrive.
//! 4. When the task wants to hear about changes, it calls
//!    `sys.gpio_irq_control` to enable the interrupt source.
//! 5. The task gets a notification when the requested type of change occurs.
//! 6. When it wants to hear about another, it calls `sys.gpio_irq_control`
//!    again.
//!
//! Like the `sys_irq_control` syscall, if you map multiple interrupt sources to
//! a single shared notification bit, they become indistinguishable from the
//! task's perspective. This is quite deliberate, because it provides us a way
//! to map a single semantic interrupt source in the code to potentially several
//! real interrupt sources in hardware. In this API specifically, this has the
//! additional limitation that all pin interrupt sources mapped to a given bit
//! must be configured identically -- it is not possible to set one to rising
//! and the other to falling, say. If this proves annoying, we will elaborate
//! it.
//!
//! From the perspective of this implementation, things work like this:
//!
//! 1. We process a config table in `generated` (end of this file) and apply
//!    settings to SYSCFG to route all pins at startup. This has no immediate
//!    effect on anything else, so we can do it eagerly.
//!
//! 2. We expect all EXTI interrupts to be routed to a single notification bit,
//!    called the exti wildcard IRQ. We enable this IRQ in the kernel at
//!    startup.
//!
//! 3. When notifications arrive, we use the "pending" registers in the EXTI to
//!    figure out what exactly happened, and post notification(s) to the
//!    appropriate tasks. We then *disable* the specific event source in the
//!    EXTI, and *re-enable* the shared IRQ in the kernel.
//!
//! 4. Repeat.
//!
//! ## Configuring EXTI notifications
//!
//! In order for a task to receive notifications about GPIO pin interrupts from
//! `sys`, we must first add the following to the task's config section in
//! `app.toml`:
//!
//! * Declare the named notification that we wish to receive (here,
//!   `"my-gpio-notification"`) in `task.<taskname>.notifications`
//! * Add a task slot for `sys` in `task.<taskname>.task-slots`, to allow the
//!   `sys.gpio_irq_configure` and `sys.gpio_irq_control` IPCs (if the task
//!   does not already depend on `sys`)
//!
//! For example:
//!
//! ```toml
//! [tasks.my-great-task]
//! # ...
//!
//! # Declare the notification this task will receive from sys
//! notifications = ["my-gpio-notification"]
//!
//! # Add a slot for sys
//! task-slots = ["sys"]
//! ```
//!
//! Next, we must add the following configurations to the `sys` task's
//! config section in `app.toml`:
//!
//! * Enable the `"exti"` feature flag in `task.sys.features`
//! * Add the `"syscfg"` and `"exti"` MMIO blocks to `tasks.sys.uses`
//! * Add interrupt configurations for all EXTI notifications provided by the
//!   target chip to `task.sys.interrupts`, mapping them all to a single
//!   notification bit in `task.sys.notifications`
//! * Add a `tasks.sys.config.gpio-irqs.<name>` section with the desired GPIO
//!   pin and port, and the name of the task and notification to which the pin's
//!   interrupt will be mapped.
//!
//! For example, on the STM32H753, we would add the following configuration to
//! post the notification named `"my-gpio-notification"` to the task
//! `my-great-task` for Pin PC13:
//!
//! ```toml
//! [tasks.sys]
//! name = "drv-stm32xx-sys"
//! priority = 1
//! start = true
//! task-slots = ["jefe"]
//!
//! # Add the "exti" feature flag here
//! features = ["h753", "exti"]
//! # Add "syscfg" and "exti" here
//! uses = ["rcc", "gpios", "system_flash", "syscfg", "exti"]
//! # The notification sent by the kernel on EXTI interrupts
//! # (you can name this whatever you want as long as it's the same name
//! # used in `tasks.sys.interrupts` below)
//! notifications = ["exti-wildcard-irq"]
//!
//! # Map all EXTI interrupts to the wildcard IRQ
//! [tasks.sys.interrupts]
//! "exti.exti0" = "exti-wildcard-irq"
//! "exti.exti1" = "exti-wildcard-irq"
//! "exti.exti2" = "exti-wildcard-irq"
//! "exti.exti3" = "exti-wildcard-irq"
//! "exti.exti4" = "exti-wildcard-irq"
//! "exti.exti9_5" = "exti-wildcard-irq"
//! "exti.exti15_10" = "exti-wildcard-irq"
//!
//! # Map the GPIO pin to the notification
//! #
//! # Up to 16 such blocks can be added, to map up to 16 GPIO pins to client
//! # task notifications.
//! [tasks.sys.config.gpio-irqs.my-gpio-notification]
//! # Declare which GPIO port and pin to receive interrupts from. Here, we
//! # want to receive interrupts from pin PC13.
//! port = "C"
//! pin = 13
//! # The name of the client task and the notification to post to it.
//! owner = { name = "my-great-task", notification = "my-gpio-notification" }
//! ```
//!
//! ## Using EXTI notifications
//!
//! Once a task has been configured to receive notifications from `sys` for EXTI
//! GPIO interrupts, as discussed above, it can use the `sys` task's IPC
//! interface to configure and control the EXTI interrupts.
//!
//! First, the task must ensure that it includes the generated constants for
//! notifications. If the task does not already include this code, it must add a
//! call to `build_util::build_notifications()` in its `build.rs`, and include
//! the generated using:
//!
//! ```rust
//! include!(concat!(env!("OUT_DIR"), "/notifications.rs"));
//! ```
//!
//! In order to receive notifications, the task must first configure the pin on
//! which it wishes to receive interrupts in input mode, using the
//! [`Sys::gpio_configure_input`] IPC interface. Optionally, if pull-up or
//! pull-down resistors are required, they may be configured by this IPC as
//! well. Then, the task must use the [`Sys::gpio_irq_configure`] IPC to
//! configure the edge sensitivity for the GPIO interrupts mapped to a
//! notification mask. Interrupts can trigger either on the rising edge, the
//! falling edge, or both. These configurations only need to be performed once,
//! unless the task wishes to change the pin's configuration at runtime.
//!
//! For example, if we wish to use the EXTI notification
//! `"my-gpio-notification"` for pin PC13, as shown in the above section, we
//! might add the following to our task's `main`:
//!
//! ```rust,no-run
//! # mod notifications { pub const MY_GPIO_NOTIFICATION_MASK: u32 = 1 << 0; }
//! use drv_stm32xx_sys_api::{PinSet, Port, Pull};
//! use userlib::*;
//!
//! task_slot!(SYS, sys);
//!
//! #[export_name = "main"]
//! pub fn main() -> ! {
//!     let sys = drv_stm32xx_sys_api::Sys::from(SYS.get_task_id());
//!
//!     // Configure the pin as an input, without pull-up or pull-down
//!     // resistors:
//!     sys.gpio_configure_input(
//!         PinSet {
//!            port: Port::C,
//!            pin_mask: (1 << 13),
//!         },
//!         Pull::None,
//!     );
//!
//!     // Now, configure the pin to trigger interrupts on the rising edge:
//!     sys.gpio_irq_configure(
//!         notifications::MY_GPIO_NOTIFICATION_MASK,
//!         Edge::Rising, // or `Edge::Falling`, `Edge::Both`
//!     );
//!
//!     // Eventually we will do other things here
//! }
//!```
//!
//! Once this is done, the task can enable a GPIO interrupt by calling the
//! [`Sys::gpio_irq_control`] IPC. This IPC takes two arguments: a mask of
//! notification bits to disable, and a mask of notification bits to enable.
//! Once the interrupt is enabled, the task will receive a notification when the
//! GPIO pin's level changes, based on the edge sensitivity configured above.
//! Once the interrupt has triggered a notification, it will be automatically
//! disabled until the task calls `gpio_irq_control` again to re-enable it.
//!
//! Thus, continuing from the above example, to wait for GPIO notifications in a
//! loop, we would write something like this:
//!
//! ```rust,no-run
//! # fn handle_interrupt() {}
//! # mod notifications { pub const MY_GPIO_NOTIFICATION_MASK: u32 = 1 << 0; }
//! use drv_stm32xx_sys_api::{PinSet, Port, Pull, Edge, IrqControl};
//! use userlib::*;
//!
//! task_slot!(SYS, sys);
//!
//! #[export_name = "main"]
//! pub fn main() -> ! {
//!     let sys = drv_stm32xx_sys_api::Sys::from(SYS.get_task_id());
//!
//!     sys.gpio_configure_input(
//!         PinSet {
//!            port: Port::C,
//!            pin_mask: (1 << 13),
//!         },
//!         Pull::None,
//!     );
//!
//!     sys.gpio_irq_configure(
//!         notifications::MY_GPIO_NOTIFICATION_MASK,
//!         Edge::Rising, // or `Edge::Falling`, `Edge::Both`
//!     );
//!
//!     // First, enable the interrupt, so that we can receive our
//!     // notification. We loop here to retry if the `sys` task has panicked.
//!     while sys
//!         .gpio_irq_control(notifications::MY_GPIO_NOTIFICATION_MASK, IrqControl::Enable)
//!         .is_err()
//!     {}
//!
//!     // Wait to recieve notifications for our GPIO interrupt in a loop:
//!     loop {
//!         // Wait for a notification.
//!         sys_recv_notification(notifications::MY_GPIO_NOTIFICATION_MASK);
//!
//!         // Call `sys.gpio_irq_control()` to both re-enable the interrupt
//!         // and ask the `sys` task to confirm for us that the interrupt has
//!         // actually fired.
//!         //
//!         // If we did *not* want to re-enable the interrupt here, we could
//!         // pass `IrqControl::Check` rather than `IrqControl::Enable`.
//!         let fired = sys
//!             .gpio_irq_control(notifications::MY_GPIO_NOTIFICATION_MASK, IrqControl::Enable)
//!             // If the `sys` task panicked, just wait for another notification.
//!             .unwrap_or(false)
//!         if fired {
//!             // If the sys task confirms that our interrupt has fired, do...
//!             // whatever it is this task is supposed to do when that happens.
//!             handle_interrupt();
//!         }
//!     }
//! }
//!```
//!
//! For a more complete example of using GPIO interrupts, see the
//! [`nucleo-user-button`] demo task, which toggles a user LED on an
//! STM32H7-NUCLEO dev board when the user button is pressed.
//!
//! [`nucleo-user-button`]: https://github.com/oxidecomputer/hubris/tree/master/task/nucleo-user-button

#![no_std]
#![no_main]
#![deny(unsafe_op_in_unsafe_fn)]

use cfg_if::cfg_if;

cfg_if! {
    if #[cfg(feature = "family-stm32g0")] {
        use stm32g0 as pac;

        #[cfg(feature = "g030")]
        use stm32g0::stm32g030 as device;

        #[cfg(feature = "g031")]
        use stm32g0::stm32g031 as device;

        #[cfg(feature = "g070")]
        use stm32g0::stm32g070 as device;

        #[cfg(feature = "g0b1")]
        use stm32g0::stm32g0b1 as device;
    } else if #[cfg(feature = "family-stm32h7")] {
        use stm32h7 as pac;

        #[cfg(feature = "h743")]
        use stm32h7::stm32h743 as device;
        #[cfg(feature = "h753")]
        use stm32h7::stm32h753 as device;
    } else {
        compile_error!("unsupported SoC family");
    }
}

use drv_stm32xx_gpio_common::{server::get_gpio_regs, Port};
use drv_stm32xx_sys_api::{Edge, Group, IrqControl, RccError};
use idol_runtime::{ClientError, NotificationHandler, RequestError};
#[cfg(not(feature = "test"))]
use task_jefe_api::{Jefe, ResetReason};

use userlib::*;

#[cfg(not(feature = "test"))]
task_slot!(JEFE, jefe);

/// This part of the world contains a _lot_ of registers that are essentially
/// indexed arrays of bits. The PAC wants us to decide which bit we are altering
/// at compile time. While there are several places in this driver where we
/// generate a bunch of extra code to do it the PAC's way, this was a bridge too
/// far.
///
/// This trait is implemented by registers that contain bit arrays, allowing
/// them to be written dynamically. All operations are `unsafe` as a concession
/// to the PAC using `unsafe` to mean "scary hardware."
trait FlagsRegister {
    /// Sets bit `index` in the register, preserving other bits.
    ///
    /// # Safety
    ///
    /// This is unsafe because, in theory, you might be able to find a register
    /// where setting a bit can imperil memory safety. It is your responsibility
    /// not to use this on such registers.
    unsafe fn set_bit(&self, index: u8);

    /// Clears bit `index` in the register, preserving other bits.
    ///
    /// # Safety
    ///
    /// This is unsafe because, in theory, you might be able to find a register
    /// where clearing a bit can imperil memory safety. It is your
    /// responsibility not to use this on such registers.
    unsafe fn clear_bit(&self, index: u8);
}

impl<S> FlagsRegister for pac::Reg<S>
where
    S: pac::RegisterSpec<Ux = u32> + pac::Readable + pac::Writable,
{
    unsafe fn set_bit(&self, index: u8) {
        self.modify(|r, w| unsafe { w.bits(r.bits() | 1 << index) });
    }

    unsafe fn clear_bit(&self, index: u8) {
        self.modify(|r, w| unsafe { w.bits(r.bits() & !(1 << index)) });
    }
}

#[export_name = "main"]
fn main() -> ! {
    // From thin air, pluck a pointer to the RCC register block.
    //
    // Safety: this is needlessly unsafe in the API. The RCC is essentially a
    // static, and we access it through a & reference so aliasing is not a
    // concern. Were it literally a static, we could just reference it.
    let rcc = unsafe { &*device::RCC::ptr() };

    cfg_if! {
        if #[cfg(feature = "exti")] {
            // EXTI routing global setup.

            // Set up external interrupt routing. This routing is not in EXTI --
            // that would be too easy! It's in SYSCFG.
            //
            // Over in SYSCFG we assign one GPIO port to each of 16 channels, or
            // rather, to the subset of channels that are actually being used in
            // our configuration table.

            // Safety: same complaint as above, this is needlessly unsafe in the
            // API the way we use peripherals.
            let syscfg = unsafe { &*device::SYSCFG::ptr() };

            for (i, entry) in dispatch_table_iter() {
                // Process entries that are filled in...
                if let &Some(ExtiDispatch { port, .. }) = entry {
                    let register = i >> 2;
                    let slot = i & 0b11;

                    // This is an array of 4-bit fields spread across 4 32-bit
                    // registers. We're indexing them with i. There is really no
                    // good way to do this with the PAC, so we get the vaguely
                    // horrible nest of match statements you see below. Its goal
                    // is to
                    // 1. Select a register based on the top two bits of the
                    //    index, and then
                    // 2. Select a field within that register based on the
                    //    bottom two.
                    // 3. Stuff the port number into that field.
                    // 4. Write it back.
                    match register {
                        0 => syscfg.exticr1.modify(|_, w| match slot {
                            // Safety: field modeled incorrectly in PAC
                            0 => unsafe { w.exti0().bits(port as u8) },
                            // Safety: field modeled incorrectly in PAC
                            1 => unsafe { w.exti1().bits(port as u8) },
                            // Safety: field modeled incorrectly in PAC
                            2 => unsafe { w.exti2().bits(port as u8) },
                            // Safety: field modeled incorrectly in PAC
                            _ => unsafe { w.exti3().bits(port as u8) },
                        }),
                        1 => syscfg.exticr2.modify(|_, w| match slot {
                            // Safety: field modeled incorrectly in PAC
                            0 => unsafe { w.exti4().bits(port as u8) },
                            // Safety: field modeled incorrectly in PAC
                            1 => unsafe { w.exti5().bits(port as u8) },
                            // Safety: field modeled incorrectly in PAC
                            2 => unsafe { w.exti6().bits(port as u8) },
                            // Safety: field modeled incorrectly in PAC
                            _ => unsafe { w.exti7().bits(port as u8) },
                        }),
                        2 => syscfg.exticr3.modify(|_, w| match slot {
                            // Safety: field modeled incorrectly in PAC
                            0 => unsafe { w.exti8().bits(port as u8) },
                            // Safety: field modeled incorrectly in PAC
                            1 => unsafe { w.exti9().bits(port as u8) },
                            // Safety: field modeled incorrectly in PAC
                            2 => unsafe { w.exti10().bits(port as u8) },
                            // Safety: field modeled incorrectly in PAC
                            _ => unsafe { w.exti11().bits(port as u8) },
                        }),
                        _ => syscfg.exticr4.modify(|_, w| match slot {
                            // Safety: field modeled incorrectly in PAC
                            0 => unsafe { w.exti12().bits(port as u8) },
                            // Safety: field modeled incorrectly in PAC
                            1 => unsafe { w.exti13().bits(port as u8) },
                            // Safety: field modeled incorrectly in PAC
                            2 => unsafe { w.exti14().bits(port as u8) },
                            // Safety: field modeled incorrectly in PAC
                            _ => unsafe { w.exti15().bits(port as u8) },
                        }),
                    }
                }
            }
        }
    }

    // Global setup.
    cfg_if! {
        if #[cfg(feature = "family-stm32g0")] {
            rcc.iopenr.write(|w| {
                w.iopaen()
                    .set_bit()
                    .iopben()
                    .set_bit()
                    .iopcen()
                    .set_bit()
                    .iopden()
                    .set_bit()
                    .iopfen()
                    .set_bit();
                #[cfg(feature = "g0b1")]
                w.iopeen().set_bit();
                w
            });
        } else if #[cfg(feature = "family-stm32h7")] {
            rcc.ahb4enr.write(|w| {
                w.gpioaen()
                    .set_bit()
                    .gpioben()
                    .set_bit()
                    .gpiocen()
                    .set_bit()
                    .gpioden()
                    .set_bit()
                    .gpioeen()
                    .set_bit()
                    .gpiofen()
                    .set_bit()
                    .gpiogen()
                    .set_bit()
                    .gpiohen()
                    .set_bit()
                    .gpioien()
                    .set_bit()
                    .gpiojen()
                    .set_bit()
                    .gpioken()
                    .set_bit()
            });
        }
    }

    // Read RCC_RSR and inform Jefe why we reset.
    //
    // If the test feature is set, don't try to talk to Jefe, as the test runner
    // runs as the supervisor, and, therefore, there is no Jefe to talk to.
    #[cfg(not(feature = "test"))]
    if let Some(reason) = try_read_reset_reason(rcc) {
        Jefe::from(JEFE.get_task_id()).set_reset_reason(reason);
    }

    // Field messages.
    let mut buffer = [0u8; idl::INCOMING_SIZE];
    let mut server = ServerImpl {
        rcc,

        #[cfg(feature = "exti")]
        // Safety: this gets a shared reference to the static EXTI instance,
        // which is an operation that can't actually be used to violate Rust
        // safety.
        exti: unsafe { &*device::EXTI::ptr() },

        #[cfg(feature = "exti")]
        exti_cpupr_2: 0,
    };

    #[cfg(feature = "exti")]
    sys_irq_control(notifications::EXTI_WILDCARD_IRQ_MASK, true);

    loop {
        idol_runtime::dispatch(&mut buffer, &mut server);
    }
}

#[cfg(feature = "exti")]
counters::counters!(__EXTI_IRQ_COUNTERS, generated::ExtiIrq);

struct ServerImpl<'a> {
    rcc: &'a device::rcc::RegisterBlock,

    /// Pointer to the EXTI registers, which are used to disambiguate and mask
    /// pin change interrupts.
    #[cfg(feature = "exti")]
    exti: &'a device::exti::RegisterBlock,

    /// A bitfield tracking which EXTI interrupts have fired since the last time
    /// their owners have called the `gpio_irq_control` IPC. This is necessary
    /// as we must unset the sending bit in the *real* EXTI_CPUPR1 register on
    /// receipt of an interrupt in order to receive another one, but we must
    /// hang onto the pending state until the task that actually uses that
    /// interrupt asks us for it.
    #[cfg(feature = "exti")]
    exti_cpupr_2: u16,
}

impl ServerImpl<'_> {
    fn unpack_raw(raw: u32) -> Result<(Group, u8), RequestError<RccError>> {
        let bit: u8 = (raw & 0x1F) as u8;
        let bus =
            Group::from_u32(raw >> 5).ok_or(RccError::NoSuchPeripheral)?;
        // TODO: this lets people refer to bit indices that are not included in
        // the Peripheral enum, which is not great. Fixing this by deriving
        // FromPrimitive for Peripheral results in _really expensive_ checking
        // code. We could do better.
        Ok((bus, bit))
    }
}

impl idl::InOrderSysImpl for ServerImpl<'_> {
    fn enable_clock_raw(
        &mut self,
        _: &RecvMessage,
        raw: u32,
    ) -> Result<(), RequestError<RccError>> {
        let (group, bit) = Self::unpack_raw(raw)?;
        enable_clock(self.rcc, group, bit);
        Ok(())
    }

    fn disable_clock_raw(
        &mut self,
        _: &RecvMessage,
        raw: u32,
    ) -> Result<(), RequestError<RccError>> {
        let (group, bit) = Self::unpack_raw(raw)?;
        disable_clock(self.rcc, group, bit);
        Ok(())
    }

    fn enter_reset_raw(
        &mut self,
        _: &RecvMessage,
        raw: u32,
    ) -> Result<(), RequestError<RccError>> {
        let (group, bit) = Self::unpack_raw(raw)?;
        enter_reset(self.rcc, group, bit);
        Ok(())
    }

    fn leave_reset_raw(
        &mut self,
        _: &RecvMessage,
        raw: u32,
    ) -> Result<(), RequestError<RccError>> {
        let (group, bit) = Self::unpack_raw(raw)?;
        leave_reset(self.rcc, group, bit);
        Ok(())
    }

    fn gpio_configure_raw(
        &mut self,
        _: &RecvMessage,
        port: Port,
        pins: u16,
        packed_attributes: u16,
    ) -> Result<(), RequestError<core::convert::Infallible>> {
        unsafe { get_gpio_regs(port) }.configure(pins, packed_attributes);
        Ok(())
    }

    fn gpio_set_reset(
        &mut self,
        _: &RecvMessage,
        port: Port,
        set_pins: u16,
        reset_pins: u16,
    ) -> Result<(), RequestError<core::convert::Infallible>> {
        unsafe { get_gpio_regs(port) }.set_reset(set_pins, reset_pins);
        Ok(())
    }

    fn gpio_toggle(
        &mut self,
        _: &RecvMessage,
        port: Port,
        pins: u16,
    ) -> Result<(), RequestError<core::convert::Infallible>> {
        unsafe { get_gpio_regs(port) }.toggle(pins);
        Ok(())
    }

    fn gpio_read_input(
        &mut self,
        _: &RecvMessage,
        port: Port,
    ) -> Result<u16, RequestError<core::convert::Infallible>> {
        Ok(unsafe { get_gpio_regs(port) }.read())
    }

    fn read_uid(
        &mut self,
        _: &RecvMessage,
    ) -> Result<[u32; 3], RequestError<core::convert::Infallible>> {
        Ok(drv_stm32xx_uid::read_uid())
    }

    fn gpio_irq_control(
        &mut self,
        rm: &RecvMessage,
        mask: u32,
        op: IrqControl,
    ) -> Result<bool, RequestError<core::convert::Infallible>> {
        // We want to only include code for this if exti is requested.
        // Unfortunately the _operation_ is available unconditionally, but we'll
        // fault any clients who call it if it's unsupported (below).
        cfg_if! {
            if #[cfg(feature = "exti")] {
                // This mask will later be used for checking the stored
                // interrupt pending state in `self.exti_cpupr_2` --- we'll put
                // a 1 here for the index of every slot that's mapped to a
                // notification in the enable/disable masks.
                //
                // We'll also use the presence of any bits in this mask in order
                // to determine whether the caller actually provided masks that
                // map to anything interesting, and reply-fault if they
                // didn't.
                let mut slot_mask = 0u16;

                for (i, _) in exti_dispatch_for(rm.sender, mask) {
                    // What bit do we touch for this entry? (Mask is to ensure
                    // that the compiler understands this shift cannot
                    // overflow.)
                    let bit = 1 << (i & 0xF);

                    // Record that these bits meant something.
                    slot_mask |= bit;

                    match op {
                        IrqControl::Enable => {
                            // Enable this source by _setting_ the
                            // corresponding mask bit.
                            self.exti.cpuimr1.modify(|r, w| {
                                let new_value = r.bits() | (bit as u32);
                                // Safety: not actually unsafe, PAC didn't
                                // model this field right
                                unsafe { w.bits(new_value) }
                            });
                        },
                        IrqControl::Disable => {
                            // Disable this source by _clearing_ the
                            // corresponding mask bit.
                            self.exti.cpuimr1.modify(|r, w| {
                                let new_value = r.bits() & !(bit as u32);
                                // Safety: not actually unsafe, PAC didn't
                                // model this field right
                                unsafe {
                                    w.bits(new_value)
                                }
                            });
                        },
                        IrqControl::Check => {
                            // We are just checking if an IRQ has triggered,
                            // so don't actually mess with the source's mask
                            // register at all.
                        }
                    }
                }

                // Check that all the set bits in the caller's provided masks
                // described interrupts that they actually define. This helps to
                // catch cases where the mask is wrong, which mostly happens
                // during development and test, and is annoying to find
                // otherwise.
                //
                // It is unfortunate that we're doing this _after_ potentially
                // having side effects above. This is a compromise: because this
                // error basically can't happen in a real program -- you have to
                // work around the configuration system to achieve it -- we
                // don't want to add too much cost to the common case of "no
                // error." But we also don't want the error to go undetected.
                if slot_mask == 0 {
                    return Err(ClientError::BadMessageContents.fail());
                }

                // Check if any interrupts are pending for the slots mapped to
                // the caller's notification masks.
                let pending = self.exti_cpupr_2 & slot_mask != 0;
                // ...and clear those bits for the next interrupt.
                self.exti_cpupr_2 &= !slot_mask;

                Ok(pending)

            } else {
                // Suppress unused variable warnings (yay conditional
                // compilation)
                let _ = (rm, mask, op);

                // Fault any clients who try to use this in an image where it's
                // not included.
                Err(ClientError::UnknownOperation.fail())
            }
        }
    }

    fn gpio_irq_configure(
        &mut self,
        rm: &RecvMessage,
        mask: u32,
        edge: Edge,
    ) -> Result<(), RequestError<core::convert::Infallible>> {
        // We want to only include code for this if exti is requested.
        // Unfortunately the _operation_ is available unconditionally, but we'll
        // fault any clients who call it if it's unsupported (below).
        cfg_if! {
            if #[cfg(feature = "exti")] {

                // Keep track of which bits in the caller-provided masks
                // actually matched things.
                let mut used_bits = 0u32;

                for (i, entry) in exti_dispatch_for(rm.sender, mask) {
                    // (Mask is to ensure that the compiler understands this
                    // shift cannot overflow.)
                    let imask = 1 << (i & 0xF);

                    used_bits |= entry.mask;

                    // Set or clear Rising Trigger Selection
                    // Register bit according to the rising flag
                    self.exti.rtsr1.modify(|r, w| {
                        let new_value = if edge.is_rising() {
                            r.bits() | imask
                        } else {
                            r.bits() & !imask
                        };
                        unsafe {
                            w.bits(new_value)
                        }
                    });

                    // Set or clear Falling Trigger Selection
                    // Register bit according to the rising flag
                    self.exti.ftsr1.modify(|r, w| {
                        let new_value = if edge.is_falling() {
                            r.bits() | imask
                        } else {
                            r.bits() & !imask
                        };
                        unsafe {
                            w.bits(new_value)
                        }
                    });
                }

                // Check that all the set bits in the caller's provided masks
                // described interrupts that they actually define. This helps to
                // catch cases where the mask is wrong, which mostly happens
                // during development and test, and is annoying to find
                // otherwise.
                //
                // It is unfortunate that we're doing this _after_ potentially
                // having side effects above. This is a compromise: because this
                // error basically can't happen in a real program -- you have to
                // work around the configuration system to achieve it -- we
                // don't want to add too much cost to the common case of "no
                // error." But we also don't want the error to go undetected.
                if mask & used_bits != mask {
                    Err(ClientError::BadMessageContents.fail())
                } else {
                    Ok(())
                }

            } else {
                // Suppress unused variable warnings (yay conditional
                // compilation)
                let _ = (rm, mask, edge);

                // Fault any clients who try to use this in an image where it's
                // not included.
                Err(ClientError::UnknownOperation.fail())
            }
        }
    }
}

#[cfg(feature = "exti")]
struct ExtiDispatch {
    port: Port,
    task: TaskId,
    mask: u32,
    name: generated::ExtiIrq,
}

/// Iterates over the indices of EXTI sources mapped to the provided
/// notification `mask` for the task with ID `task`.
#[cfg(feature = "exti")]
fn exti_dispatch_for(
    task: TaskId,
    mask: u32,
) -> impl Iterator<Item = (usize, &'static ExtiDispatch)> {
    // This is semantically equivalent to iter.enumerate, but winds up handing
    // the compiler very different code that avoids an otherwise-difficult panic
    // site on an apparently-overflowing addition (that was not actually capable
    // of overflowing).
    dispatch_table_iter().filter_map(move |(i, entry)| {
        let entry = entry.as_ref()?;
        if task.index() == entry.task.index() && mask & entry.mask != 0 {
            Some((i, entry))
        } else {
            None
        }
    })
}

impl NotificationHandler for ServerImpl<'_> {
    fn current_notification_mask(&self) -> u32 {
        cfg_if! {
            if #[cfg(feature = "exti")] {
                notifications::EXTI_WILDCARD_IRQ_MASK
            } else {
                // We don't use notifications, don't listen for any.
                0
            }
        }
    }

    fn handle_notification(&mut self, bits: u32) {
        cfg_if! {
            if #[cfg(feature = "exti")] {
                if bits & notifications::EXTI_WILDCARD_IRQ_MASK != 0 {
                    // Some combination of external pin change interrupts have
                    // been triggered! Our first task is to determine which.
                    // Fortunately, that's easy; the peripheral has a "pending"
                    // register that mirrors the NVIC's, and latches events when
                    // they occur.
                    let pending = self.exti.cpupr1.read();
                    // We'll cross-correlate that with the mask register so we
                    // only deliver events a task is actually interested in.
                    let enabled = self.exti.cpuimr1.read();

                    // The EXTI bits corresponding to pin events are in the
                    // lower 16 bits. The PAC didn't expect us to use these as a
                    // group, so we must bypass it here manually:
                    let pending = pending.bits() as u16;
                    let enabled = enabled.bits() as u16;

                    let pending_and_enabled = pending & enabled;

                    let mut bits_to_acknowledge = 0u16;

                    for pin_idx in 0..16 {
                        // TODO: this sure looks like it should be using
                        // iter.enumerate, doesn't it? Unfortunately that's not
                        // currently getting inlined by rustc, resulting in rather
                        // silly code containing panics. This is significantly
                        // smaller.
                        let entry = &generated::EXTI_DISPATCH_TABLE[pin_idx];
                        if pending_and_enabled & 1 << pin_idx != 0 {
                            // A channel is pending! We need to handle this
                            // basically like the kernel handles native hardware
                            // interrupts, which means
                            // - Post the event to the owning task
                            // - Mask the interrupt
                            // - Clear the pending bit (we have to do this
                            //   manually unlike native interrupts).

                            if let &Some(ExtiDispatch { task, mask, name, .. }) = entry {
                                counters::count!(__EXTI_IRQ_COUNTERS, name);

                                let task = sys_refresh_task_id(task);
                                sys_post(task, mask);
                            } else {
                                // spurious interrupt.
                                // TODO: probably add this to a counter; it's
                                // almost certainly a bug, but panicking would
                                // be rude since this is a shared service.
                            }

                            bits_to_acknowledge |= 1 << pin_idx;
                        }
                    }

                    if bits_to_acknowledge != 0 {
                        // Save pending bits so that when the tasks that own the
                        // interrupt(s) that fired call `Sys.gpio_irq_control` to
                        // check if their IRQs fired, we'll be able to tell them.
                        self.exti_cpupr_2 |= bits_to_acknowledge;

                        // Mask and unpend interrupts en masse to save like six
                        // cycles because we'll totally notice in practice
                        // </mild-sarcasm>

                        // Zero-extend for convenience below:
                        let bits_to_acknowledge = u32::from(bits_to_acknowledge);

                        // Mask the sources we're handling by clearing the
                        // corresponding mask bits. The EXTI unfortunately has
                        // no way to do this atomically, we need to RMW.
                        // Fortunately the mask bits are only changed by
                        // software, and only by _this_ software.
                        self.exti.cpuimr1.modify(|r, w| {
                            let new_value = r.bits() & !bits_to_acknowledge;
                            // Safety: this operation is unsafe because the PAC
                            // hasn't thought about it. Enabling an interrupt
                            // source is basically always "safe" in the Rust
                            // sense on Hubris.
                            unsafe {
                                w.bits(new_value)
                            }
                        });

                        // Clear pending. This _can_ be done atomically without
                        // risk of losing events, thank the maker. The register
                        // is a write-1-to-clear register.
                        self.exti.cpupr1.write(|w| {
                            // Safety: this is unsafe because the PAC hasn't
                            // thought about it. Clearing pending interrupt
                            // sources is safe-in-the-Rust sense in our
                            // application (and, in practice, probably in all
                            // applications).
                            unsafe {
                                w.bits(bits_to_acknowledge)
                            }
                        });
                    }

                    // Make sure we always turn this source back on.
                    sys_irq_control(
                        notifications::EXTI_WILDCARD_IRQ_MASK,
                        true,
                    );
                }
            } else {
                // prevent unused variable warning:
                let _ = bits;
                unreachable!()
            }
        }
    }
}

cfg_if! {
    if #[cfg(feature = "family-stm32g0")] {
        fn enable_clock(
            rcc: &device::rcc::RegisterBlock,
            group: Group,
            bit: u8,
        ) {
            match group {
                Group::Iop => unsafe { rcc.iopenr.set_bit(bit) },
                Group::Ahb => unsafe { rcc.ahbenr.set_bit(bit) },
                Group::Apb1 => unsafe { rcc.apbenr1.set_bit(bit) },
                Group::Apb2 => unsafe { rcc.apbenr2.set_bit(bit) },
            }
        }

        fn disable_clock(
            rcc: &device::rcc::RegisterBlock,
            group: Group,
            bit: u8,
        ) {
            match group {
                Group::Iop => unsafe { rcc.iopenr.clear_bit(bit) },
                Group::Ahb => unsafe { rcc.ahbenr.clear_bit(bit) },
                Group::Apb1 => unsafe { rcc.apbenr1.clear_bit(bit) },
                Group::Apb2 => unsafe { rcc.apbenr2.clear_bit(bit) },
            }
        }

        fn enter_reset(
            rcc: &device::rcc::RegisterBlock,
            group: Group,
            bit: u8,
        ) {
            match group {
                Group::Iop => unsafe { rcc.ioprstr.set_bit(bit) },
                Group::Ahb => unsafe { rcc.ahbrstr.set_bit(bit) },
                Group::Apb1 => unsafe { rcc.apbrstr1.set_bit(bit) },
                Group::Apb2 => unsafe { rcc.apbrstr2.set_bit(bit) },
            }
        }

        fn leave_reset(
            rcc: &device::rcc::RegisterBlock,
            group: Group,
            bit: u8,
        ) {
            match group {
                Group::Iop => unsafe { rcc.ioprstr.clear_bit(bit) },
                Group::Ahb => unsafe { rcc.ahbrstr.clear_bit(bit) },
                Group::Apb1 => unsafe { rcc.apbrstr1.clear_bit(bit) },
                Group::Apb2 => unsafe { rcc.apbrstr2.clear_bit(bit) },
            }
        }

        #[cfg(not(feature = "test"))]
        fn try_read_reset_reason(
            rcc: &device::rcc::RegisterBlock,
        ) -> Option<ResetReason> {
            // TODO map to ResetReason cases
            let bits = rcc.csr.read().bits();
            Some(ResetReason::Other(bits))
        }
    } else if #[cfg(feature = "family-stm32h7")] {
        fn enable_clock(
            rcc: &device::rcc::RegisterBlock,
            group: Group,
            bit: u8,
        ) {
            match group {
                Group::Ahb1 => unsafe { rcc.ahb1enr.set_bit(bit) },
                Group::Ahb2 => unsafe { rcc.ahb2enr.set_bit(bit) },
                Group::Ahb3 => unsafe { rcc.ahb3enr.set_bit(bit) },
                Group::Ahb4 => unsafe { rcc.ahb4enr.set_bit(bit) },
                Group::Apb1L => unsafe { rcc.apb1lenr.set_bit(bit) },
                Group::Apb1H => unsafe { rcc.apb1henr.set_bit(bit) },
                Group::Apb2 => unsafe { rcc.apb2enr.set_bit(bit) },
                Group::Apb3 => unsafe { rcc.apb3enr.set_bit(bit) },
                Group::Apb4 => unsafe { rcc.apb4enr.set_bit(bit) },
            }
        }

        fn disable_clock(
            rcc: &device::rcc::RegisterBlock,
            group: Group,
            bit: u8,
        ) {
            match group {
                Group::Ahb1 => unsafe { rcc.ahb1enr.clear_bit(bit) },
                Group::Ahb2 => unsafe { rcc.ahb2enr.clear_bit(bit) },
                Group::Ahb3 => unsafe { rcc.ahb3enr.clear_bit(bit) },
                Group::Ahb4 => unsafe { rcc.ahb4enr.clear_bit(bit) },
                Group::Apb1L => unsafe { rcc.apb1lenr.clear_bit(bit) },
                Group::Apb1H => unsafe { rcc.apb1henr.clear_bit(bit) },
                Group::Apb2 => unsafe { rcc.apb2enr.clear_bit(bit) },
                Group::Apb3 => unsafe { rcc.apb3enr.clear_bit(bit) },
                Group::Apb4 => unsafe { rcc.apb4enr.clear_bit(bit) },
            }
        }

        fn enter_reset(
            rcc: &device::rcc::RegisterBlock,
            group: Group,
            bit: u8,
        ) {
            match group {
                Group::Ahb1 => unsafe { rcc.ahb1rstr.set_bit(bit) },
                Group::Ahb2 => unsafe { rcc.ahb2rstr.set_bit(bit) },
                Group::Ahb3 => unsafe { rcc.ahb3rstr.set_bit(bit) },
                Group::Ahb4 => unsafe { rcc.ahb4rstr.set_bit(bit) },
                Group::Apb1L => unsafe { rcc.apb1lrstr.set_bit(bit) },
                Group::Apb1H => unsafe { rcc.apb1hrstr.set_bit(bit) },
                Group::Apb2 => unsafe { rcc.apb2rstr.set_bit(bit) },
                Group::Apb3 => unsafe { rcc.apb3rstr.set_bit(bit) },
                Group::Apb4 => unsafe { rcc.apb4rstr.set_bit(bit) },
            }
        }

        fn leave_reset(
            rcc: &device::rcc::RegisterBlock,
            group: Group,
            bit: u8,
        ) {
            match group {
                Group::Ahb1 => unsafe { rcc.ahb1rstr.clear_bit(bit) },
                Group::Ahb2 => unsafe { rcc.ahb2rstr.clear_bit(bit) },
                Group::Ahb3 => unsafe { rcc.ahb3rstr.clear_bit(bit) },
                Group::Ahb4 => unsafe { rcc.ahb4rstr.clear_bit(bit) },
                Group::Apb1L => unsafe { rcc.apb1lrstr.clear_bit(bit) },
                Group::Apb1H => unsafe { rcc.apb1hrstr.clear_bit(bit) },
                Group::Apb2 => unsafe { rcc.apb2rstr.clear_bit(bit) },
                Group::Apb3 => unsafe { rcc.apb3rstr.clear_bit(bit) },
                Group::Apb4 => unsafe { rcc.apb4rstr.clear_bit(bit) },
            }
        }

        #[cfg(not(feature = "test"))]
        fn try_read_reset_reason(
            rcc: &device::rcc::RegisterBlock,
        ) -> Option<ResetReason> {
            bitflags::bitflags! {
                // See RM0433 section 8.7.39 (RCC_RSR).
                #[derive(Copy, Clone, Debug, Eq, PartialEq)]
                #[repr(transparent)]
                pub struct ResetFlags: u32 {
                    const LPWR = 1 << 30;
                    const WWDG1 = 1 << 28;
                    const IWDG1 = 1 << 26;
                    const SFT = 1 << 24;
                    const POR = 1 << 23;
                    const PIN = 1 << 22;
                    const BOR = 1 << 21;
                    const D2 = 1 << 20;
                    const D1 = 1 << 19;
                    const CPU = 1 << 17;
                }
            }

            // See RM0433 section 8.4.4 table 55, which defines the collection
            // of pins set for each of the following reset situations.
            const POWER_ON_RESET: ResetFlags = ResetFlags::from_bits_truncate(
                ResetFlags::CPU.bits()
                | ResetFlags::D1.bits()
                | ResetFlags::D2.bits()
                | ResetFlags::BOR.bits()
                | ResetFlags::PIN.bits()
                | ResetFlags::POR.bits()
            );
            const PIN_RESET: ResetFlags = ResetFlags::from_bits_truncate(
                ResetFlags::CPU.bits() | ResetFlags::PIN.bits()
            );
            const BROWNOUT_RESET: ResetFlags = ResetFlags::from_bits_truncate(
                ResetFlags::CPU.bits()
                | ResetFlags::BOR.bits()
                | ResetFlags::PIN.bits()
            );
            const SYSTEM_RESET: ResetFlags = ResetFlags::from_bits_truncate(
                ResetFlags::CPU.bits()
                | ResetFlags::PIN.bits()
                | ResetFlags::SFT.bits()
            );
            const WWDG1_RESET: ResetFlags = ResetFlags::from_bits_truncate(
                ResetFlags::CPU.bits()
                | ResetFlags::PIN.bits()
                | ResetFlags::WWDG1.bits()
            );
            const IWDG1_RESET: ResetFlags = ResetFlags::from_bits_truncate(
                ResetFlags::CPU.bits()
                | ResetFlags::PIN.bits()
                | ResetFlags::IWDG1.bits()
            );
            const LOW_POWER_SECURITY_RESET: ResetFlags =
                ResetFlags::from_bits_truncate(
                    ResetFlags::CPU.bits()
                    | ResetFlags::PIN.bits()
                    | ResetFlags::LPWR.bits()
                );

            let rsr = rcc.rsr.read();
            let bits = rsr.bits();
            if bits == 0 {
                // RSR has been cleared; maybe our task has restarted? No
                // matter how we got here, we don't know why we most
                // recently reset.
                return None;
            }

            let flags = ResetFlags::from_bits_truncate(bits);
            let reason = match flags {
                POWER_ON_RESET => ResetReason::PowerOn,
                PIN_RESET => ResetReason::Pin,
                SYSTEM_RESET => ResetReason::SystemCall,
                BROWNOUT_RESET => ResetReason::Brownout,
                WWDG1_RESET => ResetReason::SystemWatchdog,
                IWDG1_RESET => ResetReason::IndependentWatchdog,
                LOW_POWER_SECURITY_RESET => ResetReason::LowPowerSecurity,
                ResetFlags::D1 | ResetFlags::D2 => ResetReason::ExitStandby,
                _ => ResetReason::Other(bits),
            };

            // Clear RSR.
            rcc.rsr.modify(|_, w| w.rmvf().set_bit());

            Some(reason)
        }
    } else {
        compile_error!("unsupported SoC family");
    }
}

#[cfg(feature = "exti")]
#[inline(always)]
fn dispatch_table_iter(
) -> impl Iterator<Item = (usize, &'static Option<ExtiDispatch>)> {
    // TODO: this sure looks like it should be using iter.enumerate, doesn't it?
    // Unfortunately that's not currently getting inlined by rustc, resulting in
    // rather silly code containing panics. This is significantly smaller.
    (0..generated::EXTI_DISPATCH_TABLE.len())
        .zip(&generated::EXTI_DISPATCH_TABLE)
}

include!(concat!(env!("OUT_DIR"), "/notifications.rs"));

mod idl {
    use super::{Edge, IrqControl, Port, RccError};

    include!(concat!(env!("OUT_DIR"), "/server_stub.rs"));
}

#[cfg(feature = "exti")]
mod generated {
    use super::*;

    include!(concat!(env!("OUT_DIR"), "/exti_config.rs"));
}
